import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
import FormData from 'form-data';
import { OpenAPIV3 } from 'openapi-types';
import { ApiError, BaseHttpRequest, OpenAPIConfig } from '@/request';
import { ApiRequestOptions } from '@/request/core/ApiRequestOptions';
import { CancelablePromise, OnCancel } from '@/request/core/CancelablePromise';
import { ApiResult } from '@/request/core/ApiResult';
import { recordError } from '@/exception/error-handle';
import createDebug from '@/contrib/debug';
import { sleep } from '@/contrib/utils';
// eslint-disable-next-line import/no-cycle
import { client, axios } from './client';
import { checkJsonSchema } from './schema-check';

const debug = createDebug('worker:interceptors');

const isDefined = <T>(value: T | null | undefined): value is Exclude<T, null | undefined> => value !== undefined && value !== null;

const isString = (value: unknown): value is string => typeof value === 'string';

const isStringWithValue = (value: unknown): value is string => isString(value) && value !== '';

const isBlob = (value: any): value is Blob => (
  typeof value === 'object'
    && typeof value.type === 'string'
    && typeof value.stream === 'function'
    && typeof value.arrayBuffer === 'function'
    && typeof value.constructor === 'function'
    && typeof value.constructor.name === 'string'
    && /^(Blob|File)$/.test(value.constructor.name)
    && /^(Blob|File)$/.test(value[Symbol.toStringTag])
);

const isFormData = (value: unknown): value is FormData => value instanceof FormData;

const isSuccess = (status: number): boolean => status >= 200 && status < 300;

const base64 = (str: string): string => {
  try {
    return btoa(str);
  } catch (err) {
    return Buffer.from(str).toString('base64');
  }
};

const getQueryString = (params: Record<string, any>): string => {
  const qs: string[] = [];

  const append = (key: string, value: unknown) => {
    qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
  };

  const process = (key: string, value: any) => {
    if (isDefined(value)) {
      if (Array.isArray(value)) {
        value.forEach((v) => {
          process(key, v);
        });
      } else if (typeof value === 'object') {
        Object.entries(value).forEach(([k, v]) => {
          process(`${key}[${k}]`, v);
        });
      } else {
        append(key, value);
      }
    }
  };

  Object.entries(params).forEach(([key, value]) => {
    process(key, value);
  });

  if (qs.length > 0) {
    return `?${qs.join('&')}`;
  }

  return '';
};

const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
  const encoder = config.ENCODE_PATH || encodeURI;

  const path = options.url
    .replace('{api-version}', config.VERSION)
    .replace(/{(.*?)}/g, (substring: string, group: string) => {
      if (options.path?.hasOwnProperty(group)) {
        return encoder(String(options.path[group]));
      }
      return substring;
    });

  const url = /https?:\/\//.test(path) ? path : `${config.BASE}${path}`; // 可自定义完整的三方请求url
  if (options.query) {
    return `${url}${getQueryString(options.query)}`;
  }
  return url;
};

export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
  if (options.formData) {
    const formData = new FormData();

    const process = (key: string, value: unknown) => {
      if (isString(value) || isBlob(value)) {
        formData.append(key, value);
      } else {
        formData.append(key, JSON.stringify(value));
      }
    };

    Object.entries(options.formData)
      // eslint-disable-next-line
      .filter(([_, value]) => isDefined(value))
      .forEach(([key, value]) => {
        if (Array.isArray(value)) {
          value.forEach((v) => process(key, v));
        } else {
          process(key, value);
        }
      });

    return formData;
  }
  return undefined;
};

type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;

const resolve = async <T>(options: ApiRequestOptions, resolver?: T | Resolver<T>): Promise<T | undefined> => {
  if (typeof resolver === 'function') {
    return (resolver as Resolver<T>)(options);
  }
  return resolver;
};

const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions, formData?: FormData): Promise<Record<string, string>> => {
  const token = await resolve(options, config.TOKEN);
  const username = await resolve(options, config.USERNAME);
  const password = await resolve(options, config.PASSWORD);
  const additionalHeaders = await resolve(options, config.HEADERS);
  const formHeaders = typeof formData?.getHeaders === 'function' && formData?.getHeaders() || {};

  const headers = Object.entries({
    Accept: 'application/json',
    ...additionalHeaders,
    ...options.headers,
    ...formHeaders,
    // eslint-disable-next-line
  }).filter(([_, value]) => isDefined(value))
    .reduce((headers, [key, value]) => ({
      ...headers,
      [key]: String(value),
    }), {} as Record<string, string>);

  if (isStringWithValue(token)) {
    headers.Authorization = `Bearer ${token}`;
  }

  if (isStringWithValue(username) && isStringWithValue(password)) {
    const credentials = base64(`${username}:${password}`);
    headers.Authorization = `Basic ${credentials}`;
  }

  if (options.body) {
    if (options.mediaType) {
      headers['Content-Type'] = options.mediaType;
    } else if (isBlob(options.body)) {
      headers['Content-Type'] = options.body.type || 'application/octet-stream';
    } else if (isString(options.body)) {
      headers['Content-Type'] = 'text/plain';
    } else if (!isFormData(options.body)) {
      headers['Content-Type'] = 'application/json';
    }
  }

  return headers;
};

const getRequestBody = (options: ApiRequestOptions): ApiRequestOptions['body'] | undefined => {
  if (options.body) {
    return options.body;
  }
  return undefined;
};

const sendRequest = async <T>(
  config: OpenAPIConfig,
  options: ApiRequestOptions,
  url: string,
  body: any,
  formData: FormData | undefined,
  headers: Record<string, string>,
  onCancel: OnCancel,
): Promise<AxiosResponse<T>> => {
  const source = axios.CancelToken.source();

  const requestConfig: AxiosRequestConfig = {
    url,
    headers,
    data: body ?? formData,
    method: options.method,
    withCredentials: config.WITH_CREDENTIALS,
    cancelToken: source.token,
  };

  onCancel(() => source.cancel('The user aborted a request.'));

  try {
    return await client.request(requestConfig);
  } catch (error) {
    const axiosError = error as AxiosError<T>;
    if (axiosError.response) {
      recordError(axiosError);
      return axiosError.response;
    }
    // throw 的error由全局捕错统一上报
    throw error;
  }
};

const getResponseHeader = (response: AxiosResponse<any>, responseHeader?: string): string | undefined => {
  if (responseHeader) {
    const content = response.headers[responseHeader];
    if (isString(content)) {
      return content;
    }
  }
  return undefined;
};

const getResponseBody = (response: AxiosResponse<any>): any => {
  if (response.status !== 204) {
    return response.data;
  }
  return undefined;
};

const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult, resp: AxiosResponse): void => {
  const errors: Record<number, string> = {
    400: 'Bad Request',
    401: 'Unauthorized',
    403: 'Forbidden',
    404: 'Not Found',
    500: 'Internal Server Error',
    502: 'Bad Gateway',
    503: 'Service Unavailable',
    ...options.errors,
  };

  const error = errors[result.status];
  if (error) {
    throw new ApiError(options, result, error);
  }

  if (!result.ok) {
    debug(result);
    debug(resp);
    throw new ApiError(options, result, 'Generic Error');
  }
};

/**
 * Request method
 * @param config The OpenAPI configuration object
 * @param options The request options from the service
 * @returns CancelablePromise<T>
 * @throws ApiError
 */
const __request = <T>(config: OpenAPIConfig, options: ApiRequestOptions, doc: OpenAPIV3.Document): CancelablePromise<T> => new CancelablePromise(async (resolve, reject, onCancel) => {
  try {
    const url = getUrl(config, options);
    const formData = getFormData(options);
    const body = getRequestBody(options);
    const headers = await getHeaders(config, options, formData);

    if (!onCancel.isCancelled) {
      const response = await sendRequest<T>(config, options, url, body, formData, headers, onCancel);
      // debug(response.data);
      const responseBody = getResponseBody(response);
      const responseHeader = getResponseHeader(response, options.responseHeader);

      const result: ApiResult = {
        url,
        ok: isSuccess(response.status),
        status: response.status,
        statusText: response.statusText,
        body: responseHeader ?? responseBody,
      };

      catchErrorCodes(options, result, response);
      checkJsonSchema(options, doc, result);
      resolve(result.body);
    }
  } catch (error) {
    reject(error);
  }
});

export class TauriHttpRequest extends BaseHttpRequest {
  public doc: OpenAPIV3.Document|null = null;

  // eslint-disable-next-line
  constructor(config: OpenAPIConfig) {
    super(config);
  }

  public override request<T>(options: ApiRequestOptions): CancelablePromise<T> {
    if (this.doc === null) {
      throw new Error('cannot found api documents.');
    }
    return __request(this.config, options, this.doc);
  }
}

interface PollOptions<R> {
  maxTimes: number;
  gap: number;
  /**
   * 成功条件，当返回false代表当前异步操作失败，需要轮询发出新的异步操作
   */
  // eslint-disable-next-line
  condition: (res: R) => boolean;
}

const defaultPollOptions = { maxTime: 3, gap: 2000 }; // 接口限流2s
export async function pollRequest<T>(
  fn: () => T,
  options: PollOptions<Awaited<T>>,
): Promise<Awaited<T>> {
  // eslint-disable-next-line
  options = { ...defaultPollOptions, ...options };
  debug('请求', options.maxTimes);
  const result = await fn();
  if (!options.condition(result) && options.maxTimes > 1) {
    // condition返回false代表失败，需要重试
    await sleep(options.gap);
    return await pollRequest(fn, { ...options, maxTimes: options.maxTimes - 1 });
  }
  return result;
}
